#include "stdafx.h"
#include "List.h"
#include "Settings.h"
#include "Theme.h"

#define SCROLLTHRESH_SMOOTH          3

#define SCROLL_FRICTION             30
#define SCROLL_SPEED_MULT           30
#define SCROLL_REFRESH_RATE         25

#define SCROLL_SPEED_ACCEL_LIMIT    60
#define SCROLL_ACCEL_PERIOD        700

#ifdef _DEBUG
#include "crtdbg.h"
#endif

CScreenDC CList::s_dcTmp;

CList::CList(int itemHeight)
  : height(0), selection(-1)
  , skipMouseAction(false)
  , startY(0), lastY(0)
  , isScrolling(false), isDragging(false)
  , scrollMax(0)
  , scrolled(0), scrollSpeed(0), scrollLastSpeed(0), scrollOffset(0)
  , scrollStart(0), scrollChange(0), scrollDuration(0)
  , scrollStartTime(0), tStartTime(0)
  , tLongTapStartTime(0)
  , defaultItemHeight(itemHeight), avgItemHeight(itemHeight)
  , hover(-1)
  , isMinimizing(false)
{
  if (defaultItemHeight == 0)
    defaultItemHeight = avgItemHeight = 1;
  scrollTimer.Create(this);
  scrollToTimer.Create(this);
  bounceTimer.Create(this);
  longTapTimer.Create(this);
  longTapIndTimer.Create(this);
}

CList::~CList()
{
  for (CListItems::iterator iter = items.begin(); iter != items.end(); iter++)
  {
    if (*iter != NULL)
      delete *iter;
  }
  items.clear();

  scrollTimer.Stop();
  scrollToTimer.Stop();
  bounceTimer.Stop();
  longTapTimer.Stop();
  longTapIndTimer.Stop();
}

void CList::Clear()
{
  for (CListItems::iterator iter = items.begin(); iter != items.end(); iter++)
  {
    if (*iter != NULL)
      delete *iter;
  }
  items.clear();
  itemPos.clear();

  selection = -1;
  hover = -1;
  isMinimizing = false;

  isScrolling = isDragging = false;
  scrolled = 0;
  scrollMax = 0;
  scrollSpeed = scrollOffset = 0;
  scrollStart = scrollChange = 0;
  scrollDuration = 0;
  scrollStartTime = tStartTime = 0;
  scrollTimer.Stop();
  scrollToTimer.Stop();
  bounceTimer.Stop();
  height = 0;
  longTapTimer.Stop();
  longTapIndTimer.Stop();
  tLongTapStartTime = 0;
}

void CList::Draw(CDCHandle dc, const CRect& rClip)
{
  DrawBackground(dc, rClip);
  DrawForeground(dc, rClip);

  if (!this->IsWindowEnabled())
  {
    s_dcTmp.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), NULL, 0, 0, BLACKNESS);
    CTheme::BltAlpha(dc, rClip.left, rClip.top, rClip.Width(), rClip.Height(), s_dcTmp, 0, 0, 120);
  }
}

void CList::DrawBackground(CDCHandle& dc, const CRect& rClip)
{
  // ******* DRAW BACKGROUND
  if (style.hbrBackground != NULL)
  {
    dc.FillRect(&rClip, style.hbrBackground);
  }
  else
  {
    CTheme::DrawGradientGDI(dc, rClip, rcClient, style.clrBackground1, style.clrBackground2);
  }
}

void CList::DrawForeground(CDCHandle& dc, const CRect& rClip)
{
  RECT rItem = rcClient;
  rItem.bottom = rItem.top - scrolled - (int)scrollOffset;

  dc.SetTextAlign(TA_LEFT);
  dc.SetBkMode(TRANSPARENT);

  for (int i = 0; i < (int) items.size(); i++)
  {
    rItem.top = rItem.bottom;
    rItem.bottom = rItem.top + items[i]->GetHeight();
    if (rItem.bottom < rClip.top)
      continue;
    if (rItem.top > rClip.bottom)
      break;

    items[i]->Draw(dc, rItem);
  }

  DrawScrollBar(dc, rClip);
}

void CList::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
  skipKeyboardAction = false;

  switch (nChar)
  {
  case VK_UP:
    if (selection < 0)
    {
      SetSelection(GetItemIdx(scrolled));
      SetHover(this->selection);
      ScrollToSelection();
    }
    else
    {
      SelectPrev();
    }
    return;

  case VK_DOWN:
    if (selection < 0)
    {
      SetSelection(GetItemIdx(scrolled));
      SetHover(this->selection);
      ScrollToSelection();
    }
    else
    {
      SelectNext();
    }
    return;

  case VK_TACTION:
    if (isScrolling)
      StopScrolling();
    StartLongTap();
    break;
  }

  if (selection != -1)
    items[selection]->OnKeyDown(m_hWnd, nChar);
}

void CList::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags)
{
  if (skipKeyboardAction)
  {
    skipKeyboardAction = false;
    return;
  }

  switch (nChar)
  {
  case VK_UP:
  case VK_DOWN:
    return;

  case VK_TACTION:
    longTapTimer.Stop();
    break;
  }

  if (selection != -1)
    items[selection]->OnKeyUp(m_hWnd, nChar);
}

void CList::OnLButtonDown(UINT nFlags, CPoint point)
{
  if (isScrolling)
  {
    scrollLastSpeed = scrollSpeed;
    if (scrollLastSpeed > SCROLL_SPEED_ACCEL_LIMIT)
      scrollLastSpeed = SCROLL_SPEED_ACCEL_LIMIT;
    if (scrollLastSpeed < -SCROLL_SPEED_ACCEL_LIMIT)
      scrollLastSpeed = -SCROLL_SPEED_ACCEL_LIMIT;
  }
  else
  {
    scrollLastSpeed = 0;
  }

  point.y -= rcClient.top;

  skipMouseAction = false;
  startY = point.y;
  lastY = point.y;

  if (isScrolling)
  {
    StopScrolling();
    skipMouseAction = true;
  }

  tStartTime = ::GetTickCount();

  int idx = GetItemIdx(point.y + scrolled);
  if (idx >= 0 && idx < (int)items.size())
  {
    RECT rc = GetItemRect(idx, false);
    items[idx]->OnMouseOver(point.x - rcClient.left - rc.left, point.y + scrolled - rc.top);
    SetSelection(idx);
    SetHover(idx);
  }

  StartLongTap();
}

void CList::OnMouseMove(UINT nFlags, CPoint point)
{
  point.y -= rcClient.top;

  int DeltaY = point.y - lastY;

  if (abs(DeltaY) > SETTINGS.GetInt(sScrollThresh) ||
      (isDragging && abs(DeltaY) > SCROLLTHRESH_SMOOTH))
  {
    longTapTimer.Stop();

    if (selection != -1)
    {
      items[selection]->OnMouseOut();
      SetSelection();
      SetHover();
    }
    if (DeltaY != 0)
    {
      scrolled -= DeltaY;
      Redraw();
    }
    lastY = point.y;
    isDragging = true;
  }
  else if (!isDragging && !skipMouseAction)
  {
    int idx = GetItemIdx(point.y + scrolled);
    if (idx != selection)
    {
      longTapTimer.Stop();

      if (selection != -1)
        items[selection]->OnMouseOut();
      if (idx >= 0 && idx < (int)items.size())
      {
        RECT rc = GetItemRect(idx, false);
        items[idx]->OnMouseOver(point.x - rcClient.left - rc.left, point.y + scrolled - rc.top);
      }
      SetSelection(idx);
      SetHover(idx);
    }
  }
}

void CList::OnLButtonUp(UINT nFlags, CPoint point)
{
  longTapTimer.Stop();

  point.y -= rcClient.top;

  if (isScrolling || isDragging || scrolled < 0 || scrolled > scrollMax)
  {
    if (selection != -1)
    {
      items[selection]->OnMouseOut();
      SetSelection();
      SetHover();
    }

    DWORD tEndTime = ::GetTickCount();

    if (scrollLastSpeed != 0 && (tEndTime - tStartTime) > SCROLL_ACCEL_PERIOD)
      scrollLastSpeed = 0;

    // invert instead of decelerating if dragged in opposite direction
    if ((scrollLastSpeed > 0 && point.y < startY) || (scrollLastSpeed < 0 && point.y > startY))
      scrollLastSpeed = 0;

    scrollSpeed = scrollLastSpeed + (point.y - startY) * SCROLL_SPEED_MULT / double(tEndTime - tStartTime);

    if (scrolled < 0)
    {
      ScrollTo(1, SETTINGS.GetInt(sScrollBounceTime));
      Redraw();
    }
    else if (scrolled > scrollMax)
    {
      ScrollTo(scrollMax - 1, SETTINGS.GetInt(sScrollBounceTime));
      Redraw();
    }
    else if (fabs(scrollSpeed) > 6)
    {
      scrollTimer.Start(SCROLL_REFRESH_RATE);
      isScrolling = true;
      Redraw();
    } 
    else
    {
      // redraw list header only
      scrollSpeed = 0;
      isScrolling = false;
    }
    isDragging = false;
  } 
  else if (!skipMouseAction)
  {
    int idx = GetItemIdx(point.y + scrolled);
    if (idx != selection)
    {
      if (selection != -1)
        items[selection]->OnMouseOut();
      if (idx >= 0 && idx < (int)items.size())
      {
        RECT rc = GetItemRect(idx, false);
        items[idx]->OnMouseOver(point.x - rcClient.left - rc.left, point.y + scrolled - rc.top);
      }
      SetSelection(idx);
      SetHover(idx);
    }

    if (selection != -1)
    {
      RECT rc = GetItemRect(selection, false);
      items[selection]->OnMouseOut();
      items[selection]->OnMouseClick(m_hWnd, point.x - rcClient.left - rc.left, point.y + scrolled - rc.top);
    }

    SetHover();
  }
}

void CList::CalculateHeights()
{
  int c = 0;
  int count = (int)items.size();
  itemPos.clear();

  if (count <= 0)
  {
    itemPos.push_back(0);
    height = 0;
    avgItemHeight = defaultItemHeight;
    return;
  }

  itemPos.reserve(count + 1);

  for (int i = 0; i < count; i++)
  {
    itemPos.push_back(c);
    c += items[i]->GetHeight();
  }

  itemPos.push_back(c);
  height = c;
  avgItemHeight = height / count;
  UpdateScrollMax();
}

void CList::SetSelection(int idx, bool redraw)
{
  if (idx != selection)
  {
    if (selection != -1)
    {
      if (redraw && IsItemVisible(selection))
        ::InvalidateRect(m_hWnd, &GetItemClientRect(selection), FALSE);
    }
    selection = idx;
    if (selection != -1)
    {
      if (redraw && IsItemVisible(selection))
        ::InvalidateRect(m_hWnd, &GetItemClientRect(selection), FALSE);
    }
  }
}

void CList::SetHover(int idx, bool redraw)
{
  if (idx != hover)
  {
    if (hover != -1)
    {
      if (redraw && IsItemVisible(hover))
        ::InvalidateRect(m_hWnd, &GetItemClientRect(hover), FALSE);
      items[hover]->SetStateBit(lisHovered, false);
    }
    hover = idx;
    if (hover != -1)
    {
      if (redraw && IsItemVisible(hover))
        ::InvalidateRect(m_hWnd, &GetItemClientRect(hover), FALSE);
      items[hover]->SetStateBit(lisHovered, true);
    }
  }
}

int CList::GetItemIdx(int y) const
{
  if (y < 0 || y > height || items.size() == 0)
    return -1;

  y = min(height - 1, y);
  y = max(0, y);

  // estimate based on Theme::iListItemSize
  int guess = y / avgItemHeight;
  int max = (int)items.size() - 1;
  if (guess > max)
    guess = max;
  if (guess < 0)
    guess = 0;

  while (guess > 0 && y < itemPos[guess])
  { guess--; }

  while (guess < max && y >= itemPos[guess+1])
  { guess++; }

  return guess;
}

int CList::GetItemIdx(const CListItem* item) const
{
  for (int i = 0; i < (int) items.size(); i++)
  {
    if (item == items[i])
      return i;
  }

  return -1;
}

RECT CList::GetItemRect(int idx, bool screen) const
{
  RECT rc = {0};
  if (idx < 0 || idx >= (int)items.size())
    return rc;

  if (screen)
  {
    GetWindowRect(&rc);
    rc.top -= scrolled;
  }
  else
  {
    rc.top = rc.bottom = 0;
    rc.left = 0;
    rc.right = rcClient.Width();
  }

  rc.top += itemPos[idx];
  rc.bottom = rc.top + items[idx]->GetHeight();
  return rc;
}

RECT CList::GetItemClientRect(int idx) const
{
  RECT rc = GetItemRect(idx);
  ScreenToClient(&rc);
  return rc;
}

void CList::Add(CListItem* pItem)
{
  if (pItem != NULL)
  {
    pItem->SetList(this);

    if (pItem->GetHeight() == 0)
      pItem->SetHeight(defaultItemHeight);

    items.push_back(pItem);
    if (itemPos.size() > 0)
      itemPos[itemPos.size() - 1] = height;
    else
      itemPos.push_back(height);
    height += pItem->GetHeight();
    itemPos.push_back(height);
    UpdateScrollMax();
  }
}

bool CList::IsItemVisible(int index) const
{
  if (index < 0 || index >= (int)items.size())
    return false;

  return (itemPos[index+1] - scrolled >= 0 && itemPos[index] - scrolled <= rcClient.Height());
}

void CList::SelectPrev()
{
  if (this->items.size() == 0)
    return;

  if (this->selection > 0)
  {
    SetSelection(this->selection - 1);
    SetHover(this->selection);
    ScrollToSelection();
  }
}

void CList::SelectNext()
{
  if (this->items.size() == 0)
    return;

  if (this->selection < 0)
    this->selection = -1;

  if (this->selection + 1 < (int)this->items.size())
  {
    SetSelection(this->selection + 1);
    SetHover(this->selection);
    ScrollToSelection();
  }
}

void CList::ScrollToSelection(bool smoothScroll)
{
  if (selection == -1)
    return;

  // Scroll
  int pos = itemPos[selection];

  if (pos < scrolled ||
      pos + items[selection]->GetHeight() > rcClient.Height() + scrolled ||
      scrolled > scrollMax)
  {
    if (smoothScroll)
      ScrollTo(pos, SETTINGS.GetInt(sScrollSearchTime));
    else
      SetScroll(pos);
  }
}

void CList::OnScrollTimer(UINT nID, DWORD dwTime)
{
  scrollOffset = 0;
  if (fabs(scrollSpeed) < 1.0)
  {
    StopScrolling();
    Redraw();
  }
  else
  {
    isScrolling = true;
    scrollSpeed = scrollSpeed - scrollSpeed / SCROLL_FRICTION;
    MoveList(scrollSpeed);
  }
}

void CList::OnScrollToTimer(UINT nID, DWORD dwTime)
{
  double dt = (double)(::GetTickCount() - scrollStartTime);

  if (dt > scrollDuration)
  {
    dt = scrollDuration;
    StopScrolling();
    Redraw();
  }

  // Cubic
  double pos = scrollChange * (pow(dt/scrollDuration - 1, 3) + 1) + scrollStart;
  if (scrolled != (int)pos)
  {
    scrolled = (int)pos;
    Redraw();
  }
}

void CList::OnBounceTimer(UINT nID, DWORD dwTime)
{
  if (fabs(scrollOffset) <= 1.0)
  {
    bounceTimer.Stop();
    scrollOffset = 0;
    Redraw();
  }
  else
  {
    double newOffset = scrollOffset - scrollOffset * 4 / SCROLL_FRICTION;
    if ((int)newOffset != (int)scrollOffset)
      Redraw();
    scrollOffset = newOffset;
  }
}

void CList::MoveList(double delta)
{
  int tmpScrolled = scrolled - (int)delta;
  bool bounce = false;

  if (isScrolling && scrolled > 0 && tmpScrolled <= 0)
  {
    bounce = true;
    tmpScrolled = 0;
  }
  else if (isScrolling && scrolled < scrollMax && tmpScrolled >= scrollMax)
  {
    bounce = true;
    tmpScrolled = scrollMax;
  }
  if (bounce)
  {
    StopScrolling();
    scrollOffset = -delta;
    bounceTimer.Start(SCROLL_REFRESH_RATE);
  }

  if (scrolled != tmpScrolled)
  {
    scrolled = tmpScrolled;
    Redraw();
  }
}

void CList::ScrollTo(int pos, int time)
{
  StopScrolling();
  scrollOffset = 0;
  bounceTimer.Stop();

  if (pos < 0)
    pos = 0;
  if (pos > scrollMax)
    pos = scrollMax;

  if (pos == scrolled)
    return;

  scrollStart = scrolled;
  scrollChange = pos - scrolled;
  scrollDuration = time;
  scrollStartTime = ::GetTickCount();
  isScrolling = true;
  scrollToTimer.Start(SCROLL_REFRESH_RATE);
}

void CList::StopScrolling()
{
  scrollTimer.Stop();
  scrollToTimer.Stop();
  isScrolling = false;
  scrollSpeed = 0;
}

void CList::OnLongTapTimer(UINT nID, DWORD dwTime)
{
  longTapTimer.Stop();

  OnLongTap();
}

void CList::OnLongTapIndTimer(UINT nID, DWORD dwTime)
{
  if (!longTapTimer.IsActive())
  {
    longTapIndTimer.Stop();
  }
  else if (selection != -1 && items[selection]->IsLongTapValid(m_hWnd))
  {
    int progress = ((dwTime - tLongTapStartTime) * 100) / SETTINGS.GetInt(sLongTapDelay);
    HDC hdc = GetDC();
    DrawLongTapInd(hdc, selection, progress);
    ReleaseDC(hdc);
  }
}

void CList::OnLongTap()
{
  if (selection != -1 && items[selection]->IsLongTapValid(m_hWnd))
  {
    HDC hdc = GetDC();
    DrawLongTapInd(hdc, selection, 100);
    ReleaseDC(hdc);

    if (items[selection]->OnLongTap(m_hWnd))
    {
      skipMouseAction = true;
      skipKeyboardAction = true;
    }

    RECT rcItem = GetItemRect(selection);
    InvalidateRect(&rcItem, FALSE);
  }
}

void CList::StartLongTap()
{
  if (SETTINGS.GetInt(sLongTapDelay) > 0)
  {
    int time = SETTINGS.GetInt(sLongTapDelay);
    longTapTimer.Start(time);
    tLongTapStartTime = ::GetTickCount();
    longTapIndTimer.Start(time / 10);
  }
}

void CList::DrawLongTapInd(HDC hdc, int item, int progress)
{
  if (progress < 0)
    progress = 0;
  if (progress > 100)
    progress = 100;

  RECT rc = GetItemRect(item);
  // crop
  rc.bottom = min(rc.bottom, rcClient.bottom);
  rc.top = max(rc.top, rcClient.top);
  // bar
  rc.top = rc.bottom - ((rc.bottom - rc.top) * progress) / 100;
  // draw
  rc.right = rcClient.right;
  rc.left = rcClient.right - CTheme::iLongTapIndWidth;

  FillRect(hdc, &rc, progress == 100 ? CTheme::brLongTapFull : CTheme::brLongTap);
}

void CList::DrawScrollBar(HDC hdc, const RECT& rClip)
{
  if (CTheme::iListScrollbarWidth == 0)
    return;

  RECT rBar = rcClient;
  rBar.left = rBar.right - CTheme::iListScrollbarWidth;
  FillRect(hdc, &rBar, CTheme::brListScrollbarBg);

  if (height == 0)
    return;

  rBar.top = rcClient.top + scrolled * rcClient.Height() / height;
  rBar.bottom = rBar.top + rcClient.Height() * rcClient.Height() / height;
  FillRect(hdc, &rBar, CTheme::brListScrollbar);
}